package com.common.orm;

import com.common.orm.sqlparameters.SQLParameterException;
import com.common.orm.sqlparameters.SqlParameter;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @author Dmitry Savchenko
 */
public class Orm<T> {

    private static Connection connection;
    private ClassData classData;
    private SqlStringBuilder sqlStringBuilder;
    private ArrayList<Placeholder> placeholders = new ArrayList<Placeholder>();
    private Pattern argumentPattern = Pattern.compile("\\:(\\w+)\\:");//pattern that help to translate :parameter: to ? jdbc parameter
    private PreparedStatementFillingProcessor fillingProcessor;

    public Orm(Class<T> clazz) {
        init(clazz);
    }

    private void init(Class clazz) {
        classData = ClassDetailsCache.get(clazz);
    }

    public Orm setFillingProcessor(PreparedStatementFillingProcessor fillingProcessor) {
        this.fillingProcessor = fillingProcessor;
        return this;
    }

    public Orm setSqlCondition(String sql) {
        getSqlStringBuilder().setConditionSqlPart(sql);
        return this;
    }

    public Orm setSqlDeclaration(String sql) {
        getSqlStringBuilder().setDeclarationSqlPart(sql);
        return this;
    }

    public Orm setSqlEnding(String sql) {
        getSqlStringBuilder().setEndSqlString(sql);
        return this;
    }

    public Orm setProcessedFields(String... fields) {
        getSqlStringBuilder().setProcessedFields(fields);
        return this;
    }

    public Orm setFullSqlRequest(String sql) {
        getSqlStringBuilder().setFullSqlRequest(sql);
        return this;
    }

    public List<T> selectObjects(SqlParameter... parameters) throws SQLException {
        String sql = getSqlStringBuilder().formatGetSql();
        sql = processSql(sql);
        SqlExecuterWithResult<T> executer = new SqlExecuterWithResult(getConnection(), sql, classData, parameters);
        if (fillingProcessor != null) {
            fillingProcessor.fillPrepareStatement(executer.preparedStatement);
        }
        executer.fillWithParams(1, placeholders);
        List<T> list = executer.execute(classData);
        return list;
    }

    public T selectObject(SqlParameter... parameters) throws SQLException {
        getSqlStringBuilder().setEndSqlString("LIMIT 1");
        String sql = getSqlStringBuilder().formatGetSql();
        sql = processSql(sql);
        SqlExecuterWithResult<T> executer = new SqlExecuterWithResult(getConnection(), sql, classData, parameters);
        if (fillingProcessor != null) {
            fillingProcessor.fillPrepareStatement(executer.preparedStatement);
        }
        executer.fillWithParams(1, placeholders);
        List<T> list = executer.execute(classData);
        if (!list.isEmpty()) {
            return list.get(0);
        }

        return null;
    }
    
    public T selectObject(int primaryKey) throws SQLException {
        if(classData.getPrimaryKey()==null){
            throw new SQLParameterException("Class "+classData.getClazz().getName()+" does not contain primary key");
        }
        getSqlStringBuilder().setEndSqlString("LIMIT 1");
        String primKeyName=classData.getPrimaryKey().getColumn();
        getSqlStringBuilder().setConditionSqlPart(primKeyName+"="+ String.valueOf(primaryKey));
        String sql = getSqlStringBuilder().formatGetSql();
        
        sql = processSql(sql);
        SqlExecuterWithResult<T> executer = new SqlExecuterWithResult(getConnection(), sql, classData, new SqlParameter[0]);
        if (fillingProcessor != null) {
            fillingProcessor.fillPrepareStatement(executer.preparedStatement);
        }
        executer.fillWithParams(1, placeholders);
        List<T> list = executer.execute(classData);
        if (!list.isEmpty()) {
            return list.get(0);
        }

        return null;
    }

    public int executeDelete(SqlParameter... parameters) throws SQLParameterException, SQLException {
        return executeDelete(null, parameters);
    }

    public int executeDelete(T object, SqlParameter... parameters) throws SQLParameterException, SQLException {
        String sql = getSqlStringBuilder().formatDeleteSql(classData);
        sql = processSql(sql);
        SqlExecuterWithoutResult executer = new SqlExecuterWithoutResult(connection, sql, classData);
        if (fillingProcessor != null) {
            fillingProcessor.fillPrepareStatement(executer.preparedStatement);
        }
        if (getSqlStringBuilder().getConditionSQL() == null) {
            SqlParameter param = OrmUtils.getSqlParameterFromString(classData, object, classData.getPrimaryKey().getColumn());
            executer.processParameters(param);
        }
        executer.processParameters(parameters);
        executer.fillWithParams(1, placeholders);
        return executer.execute();
    }

    public int executeInsert(T object) throws SQLParameterException, SQLException {
        return executeInsert(object, true);
    }

    private boolean checkAutoPrimaryKey() {
        if (classData.getPrimaryKey() == null) {
            return false;
        }
        return true;
    }

    public int executeInsert(T object, boolean autoPrimaryKey) throws SQLParameterException, SQLException {
        if (autoPrimaryKey) {
            if (!checkAutoPrimaryKey()) {
                throw new SQLParameterException("You have set [autoPrimaryKey=true], but onject [" + object.getClass().getName() + "] does not contain annotation [PrimaryKey]");
            }
        }
        String sql = getSqlStringBuilder().formatInsertSql(classData, autoPrimaryKey);
        SqlExecuterWithoutResult executer = new SqlExecuterWithoutResult(connection, sql, classData);
        if (fillingProcessor != null) {
            fillingProcessor.fillPrepareStatement(executer.preparedStatement);
        }
        return executer.insert(classData, object, autoPrimaryKey, getSqlStringBuilder().getProcessedFields());
    }

    /**
     * Saves current object. Execute UPDATE SQL statement<br/> Does not update
     * primary key, and automatically uses primary key in WHERE statement
     *
     * @return count of modified rows
     */
    public int executeUpdate(T object) throws SQLParameterException, SQLException {
        return executeUpdate(object, false, true);
    }

    /**
     * @param usePrimaryKey use primary key to identify record i.e UPDATE
     * MYTABLE SET ... WHERE PRIM_KEY=:VALUE:
     * @param updatePrimaryKey set the primary key into the SET section of the
     * update sql
     * @return count of the modified rows
     */
    public int executeUpdate(T object, boolean updatePrimaryKey, boolean usePrimaryKey) throws SQLParameterException, SQLException {
        String sql = getSqlStringBuilder().formatUpdateSql(classData, usePrimaryKey, updatePrimaryKey);
        sql = processSql(sql);
        SqlParameter[] params = createUpdateParameterList(object, classData);
        SqlExecuterWithoutResult executer = new SqlExecuterWithoutResult(connection, sql, classData, params);
        if (fillingProcessor != null) {
            fillingProcessor.fillPrepareStatement(executer.preparedStatement);
        }
        executer.fillWithParams(1, placeholders);
        return executer.execute();
    }

    private SqlParameter[] createUpdateParameterList(T object, ClassData classData) throws SQLParameterException {
        ArrayList<SqlParameter> parameters = new ArrayList<SqlParameter>(50);
        if (getSqlStringBuilder().getProcessedFields() != null) {
            String[] fields = getSqlStringBuilder().getProcessedFields();
            for (String field : fields) {
                SqlParameter param = OrmUtils.getSqlParameterFromString(classData, object, field);
                parameters.add(param);
            }

            parameters.add(OrmUtils.getSqlParameterFromString(classData, object, classData.getPrimaryKey().getColumn()));
        } else {
            for (MethodColumnPair mcp : classData.getColumns()) {
                SqlParameter param = OrmUtils.getSqlParameterFromString(classData, object, mcp.getColumn());
                parameters.add(param);
            }
        }

        return parameters.toArray(new SqlParameter[0]);
    }

    private String processSql(String sql) {
        placeholders = getSqlParameterPlaceholders(sql);
        return replacePlaceholders(sql);
    }

    private SqlStringBuilder getSqlStringBuilder() {
        if (sqlStringBuilder == null) {
            sqlStringBuilder = new SqlStringBuilder(classData);
        }
        return sqlStringBuilder;
    }

    public static void init(Connection connection) {
        Orm.connection = connection;
    }

    public static Connection getConnection() {
        return connection;
    }

    private ArrayList<Placeholder> getSqlParameterPlaceholders(String sql) {
        ArrayList<Placeholder> parameters = new ArrayList<Placeholder>(20);
        Matcher m = argumentPattern.matcher(sql);
        int position = 0;
        while (m.find()) {
            parameters.add(new Placeholder(position, m.group(1)));
            position++;
        }
        return parameters;
    }

    //replace placeholders with ?
    private String replacePlaceholders(String sql) {
        Matcher m = argumentPattern.matcher(sql);
        sql = m.replaceAll("?");
        return sql;
    }

    public static int lastval() {
        try {
            String sql = "select last_insert_rowid()";
            PreparedStatement ps = connection.prepareStatement(sql);
            ResultSet rs = ps.executeQuery();
            int result = rs.getInt(1);
            rs.close();
            ps.close();
            return result;
        } catch (Exception ex) {
            Logger.getLogger(Orm.class.getName()).log(Level.SEVERE, null, ex);
        }
        return -1;
    }
}
